package com.atguigu.tingshu.common.login;

import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import jakarta.servlet.http.HttpServletRequest;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Aspect//声明切面类
@Component//将切面类注入到ioc容器
@Slf4j//开启日志 可以直接用log.info() log.error()来打印日志
public class GuiguLoginAspect {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 验证登录状态 获取用户ID通知逻辑
     * execution(* com.atguigu.tingshu.*.api.*.*(..)) && @annotation(guiGuLogin)
     * 表示在com.atguigu.tingshu这个包下所有的含api的路径都会执行guiGuLogin这个注解
     * @param joinPoint
     * @param guiguLogin
     * @return
     * 切面类整体思路
     * 1.写环绕通知 即目标方法执行前后均需执行的通知
     * 2.前置通知目标：获取token构建key从缓存中获取用户信息userInfoVo 获取token需要用到requestContextHolder
     * 通过requestContextHolder可以获取requestAttributes 然后通过强转的方式可以把requestAttributes接口转换为ServletRequestAttributes实现类
     * 通过实现类可以获取请求对象 通过请求对象可以获取请求头token 然后通过token可以构建key 然后可以从缓存中取用户信息
     * 然后判断注解是否要求必须登录及用户信息是否存在 如果注解要求必须登录且用户信息为null 则抛异常
     * 如果用户信息存在 则获取用户信息 并将用户信息存入到本地线程
     *3.执行目标方法
     *4.将本地线程中数据手动清理 避免出现oom 内存溢出
     *5.在保存专辑下面使用@GuiguLogin注解验证
     */
    @SneakyThrows
    @Around("execution(* com.atguigu.tingshu.*.api.*.*(..)) && @annotation(guiguLogin)")
    public Object loginAspect(ProceedingJoinPoint joinPoint,GuiguLogin guiguLogin){
        Object resultObject = new Object();
        log.info("前置通知...");
        //获取小程序端（客户端）提交令牌token
        //1.1通过请求上下文对象RequestContextHolder获取请求属性对象
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        //1.2将requestAttributes(接口）转为servletRequestAttributes（实现类）
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
        //1.3获取请求对象
        HttpServletRequest request = servletRequestAttributes.getRequest();
        //1.4基于请求对象获取请求头：token
        String token = request.getHeader("token");
        //2.查询存放在redis中的用户信息（登录成功后存入）
        //2.1构建用户登录信息key
        String loginKey = RedisConstant.USER_LOGIN_KEY_PREFIX + token;
        //2.2查询存储在Redis中的用户信息
        UserInfoVo userInfoVo = (UserInfoVo) redisTemplate.opsForValue().get(loginKey);
        //3.判断注解是否要求必须登录，如果要求登录且用户信息为空，抛出异常业务状态：208 引导用户登录
        if(guiguLogin.required()&&userInfoVo==null){
            throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
        }
        //4.如果用户信息存在，将用户id存入到ThreadLocal
        if(userInfoVo!= null){
            AuthContextHolder.setUserId(userInfoVo.getId());
        }
        //二 执行目标方法
        resultObject = joinPoint.proceed();
        //5.将ThreadLocal中数据手动清理避免OOM出现
        AuthContextHolder.removeUserId();
        log.info("后置通知...");
        return resultObject;
    }



}
